import java.awt.*;

import javax.swing.*;
import javax.sound.midi.*;

import java.util.*;
import java.awt.event.*;

public class BeatBox {					// All-in-one class, we pack all listeners and logic and GUI in this class
	
	JPanel mainPanel;					// Panel 
	ArrayList<JCheckBox> checkBoxList;	// Checkboxes for setting up track key events
	Sequencer sequencer;
	Sequence sequence;
	Track track;
	JFrame theFrame;					// Frame is rectangle of window
	
	String[] instrumentNames = {		// Instrument labels
			"Bass Drum",
			"Closed Hi-Hat", 
			"Open Hi-Hat",
			"Aqoustic Snare",
			"Crash Cymbal",
			"Hand Clap",
			"High Tom",
			"Hi Bongo",
			"Maracas",
			"Whistle",
			"Low Conga",
			"Cowbell",
			"Vibraslap",
			"Low-mid Tom",
			"High Agogo",
			"Open Hi Conga"
	};
	int[] instruments = {35, 42, 46, 38, 49, 39, 50, 60, 70, 72, 64, 56, 58, 47, 67, 63}; // Instrument codes
	
	public static void main(String[] args) {
		new BeatBox().buildGUI();				// We need to create instance, because main is static (for this chapter we don't use singletones)
	}
	
	public void buildGUI() {
		theFrame = new JFrame("Cyber BeatBox");
		theFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		BorderLayout layout = new BorderLayout();		// BorderLayout places controls at one of the borders (east, west, south, north) or at center of container
		JPanel background = new JPanel(layout);
		background.setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); // empty border around all controls
		
		checkBoxList = new ArrayList<JCheckBox>();
		Box buttonBox = new Box(BoxLayout.Y_AXIS);		// Container that uses BoxLayout, orientation - vertical (Y)
		
		JButton start = new JButton("Start");
		start.addActionListener(new StartListener());
		buttonBox.add(start);
		
		JButton stop = new JButton("Stop");
		stop.addActionListener(new StopListener());
		buttonBox.add(stop);
		
		JButton upTempo = new JButton("Tempo Up");
		upTempo.addActionListener(new UpTempoListener());
		buttonBox.add(upTempo);
		
		JButton downTempo = new JButton("Tempo Down");
		downTempo.addActionListener(new DownTempoListener());
		buttonBox.add(downTempo);
		
		Box nameBox = new Box(BoxLayout.Y_AXIS);		// The same vertical BoxLayout for 16 instrument labels
		for (int i = 0; i < 16; i++) {
			nameBox.add(new Label(instrumentNames[i]));
		}
		
		background.add(BorderLayout.EAST, buttonBox);	// Buttons are placed at right side
		background.add(BorderLayout.WEST, nameBox);		// Labels are placed at left side
		
		theFrame.getContentPane().add(background);		// We place "background" control on the frame
		
		GridLayout grid = new GridLayout(16, 16);
		grid.setVgap(1);
		grid.setHgap(2);
		mainPanel = new JPanel(grid);
		background.add(BorderLayout.CENTER, mainPanel); // GridLayout with checkboxes is placed at the center of the mainPanel
		
		for (int i = 0; i < 256; i++) {
			JCheckBox c = new JCheckBox();
			c.setSelected(false);	// We ensure that all checkboxes are in unchecked state
			checkBoxList.add(c);	// We save chekboxes to "virtual" array (not on screen)
			mainPanel.add(c);		// We add checkboxes to mainPanel (on screen)
		}
		
		setUpMidi();
		
		theFrame.setBounds(50, 50, 300, 300);
		theFrame.pack();
		theFrame.setVisible(true);	// Causes this Window to be sized to fit the preferred size and layouts of its subcomponents. The resulting width and height of the window are automatically enlarged if either of dimensions is less than the minimum size as specified by the previous call to the setMinimumSize method.
	}
	
	public void setUpMidi() {		// Midi subsystem initialization
		try {
			sequencer = MidiSystem.getSequencer(); // Sequencer is player. It plays sequences
			sequencer.open();
			sequence = new Sequence(Sequence.PPQ, 4);
			track = sequence.createTrack();			// Each sequence contain tracks
			sequencer.setTempoInBPM(120);
		} 
		catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void buildTrackAndStart() {
		int[] trackList = null;
		
		sequence.deleteTrack(track);				// Re-creating track
		track = sequence.createTrack();
		
		for (int i = 0; i < 16; i++) {
			trackList = new int[16];
			int key = instruments[i];
			
			for (int j = 0; j < 16; j++) {
				JCheckBox jc = (JCheckBox)checkBoxList.get(j + (16*i));
				if (jc.isSelected()) {				// We don't handle clicks on checkboxes - we check each checkbox on re-creating sequence
					trackList[j] = key;
				} else {
					trackList[j] = 0;
				}
			}
			
			makeTracks(trackList);
			track.add(makeEvent(176, 1, 127, 0, 16));
		}
		
		track.add(makeEvent(192, 9, 1, 0, 15));
		try {
			sequencer.setSequence(sequence);
			sequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);
			sequencer.start();
			sequencer.setTempoInBPM(120);
		} 
		catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public class StartListener implements ActionListener {
		public void actionPerformed(ActionEvent a) {
			buildTrackAndStart();
		}
	}
	
	public class StopListener implements ActionListener {
		public void actionPerformed(ActionEvent a) {
			sequencer.stop();
		}
	}
	
	public class UpTempoListener implements ActionListener {
		public void actionPerformed(ActionEvent a) {
			float tempoFactor = sequencer.getTempoFactor();
			sequencer.setTempoFactor((float) (tempoFactor * 1.03));
		}
	}
	
	public class DownTempoListener implements ActionListener {
		public void actionPerformed(ActionEvent a) {
			float tempoFactor = sequencer.getTempoFactor();
			sequencer.setTempoFactor((float) (tempoFactor * 0.97));
		}
	}
	
	public void makeTracks(int[] list) {
		for (int i = 0; i < 16; i++) {
			int key = list[i];
			if (key != 0) {
				track.add(makeEvent(144, 9, key, 100, i));	// makeEvent is just automation of the pack of instructions for creating MidiEvent
				track.add(makeEvent(128, 9, key, 100, i));	// Commands: 144 - ON, 128 - OFF
			}
		}
	}
	
	public MidiEvent makeEvent(int cmd, int chan, int one, int two, int tick) {
		MidiEvent event = null;
		try {
			ShortMessage a = new ShortMessage();
			a.setMessage(cmd, chan, one, two);
			event = new MidiEvent(a, tick);
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		return event;
	}
	
}
